package gapidt.wsdl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;

import gapidt.analysis.APIDesignAnalysisUtils;
import gapidt.analysis.ASTUtils;
import gapidt.common.StringUtils;

import org.apache.log4j.Logger;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import com.predic8.schema.ComplexType;
import com.predic8.schema.Element;
import com.predic8.schema.Schema;
import com.predic8.schema.Sequence;
import com.predic8.wsdl.Definitions;
import com.predic8.wsdl.Documentation;
import com.predic8.wsdl.Fault;
import com.predic8.wsdl.Message;
import com.predic8.wsdl.Operation;
import com.predic8.wsdl.PortType;

public class Java2WSDL11 extends Java2WSDL{
	
	private static Logger logger = Logger.getLogger(Java2WSDL11.class);
	
	protected static String SCHEMA_BASE_PREFIX = "xsd";
	
	protected Map<String, QName> primitiveXsdTypes = new HashMap<String, QName>();
	protected Map<String, QName> userDefinedTypes = new HashMap<String, QName>();
	protected static String javaArrayTypes = "java.util.AbstractCollection java.util.Collection" +
			" java.util.AbstractList java.util.List java.util.ArrayList java.util.LinkedList" +
			" java.util.AbstractSet java.util.Set java.util.SortedSet java.util.HashSet java.util.LinkedHashSet java.util.Stack java.util.TreeSet"+
			" java.util.Vector";
	protected static String javaMapTypes = "java.util.Map java.util.HashMap java.util.LinkedHashMap";
	
	protected String schemaPrefix = SCHEMA_BASE_PREFIX+"1";
	
	public Java2WSDL11(Definitions wsdl, Schema schema, boolean enclosedSchema) {
		super(wsdl, schema, enclosedSchema);
		
		initPrimitiveTypes();
		
		ASTUtils.clearCache();
		
		initSchema();
	}

	protected void initPrimitiveTypes(){
		primitiveXsdTypes.put("boolean", new QName(Schema.SCHEMA_NS, "boolean"));
		primitiveXsdTypes.put("java.lang.Boolean", new QName(Schema.SCHEMA_NS, "boolean"));
		primitiveXsdTypes.put("java.lang.Byte", new QName(Schema.SCHEMA_NS, "byte"));
		primitiveXsdTypes.put("byte", new QName(Schema.SCHEMA_NS, "byte"));
		primitiveXsdTypes.put("java.lang.Double", new QName(Schema.SCHEMA_NS, "double"));
		primitiveXsdTypes.put("double", new QName(Schema.SCHEMA_NS, "double"));
		primitiveXsdTypes.put("java.lang.Float", new QName(Schema.SCHEMA_NS, "float"));
		primitiveXsdTypes.put("float", new QName(Schema.SCHEMA_NS, "float"));
		primitiveXsdTypes.put("java.lang.Integer", new QName(Schema.SCHEMA_NS, "integer"));
		primitiveXsdTypes.put("int", new QName(Schema.SCHEMA_NS, "integer"));
		primitiveXsdTypes.put("java.lang.Long", new QName(Schema.SCHEMA_NS, "long"));
		primitiveXsdTypes.put("long", new QName(Schema.SCHEMA_NS, "long"));
		primitiveXsdTypes.put("java.lang.Short", new QName(Schema.SCHEMA_NS, "short"));
		primitiveXsdTypes.put("short", new QName(Schema.SCHEMA_NS, "short"));
		primitiveXsdTypes.put("java.lang.String", new QName(Schema.SCHEMA_NS, "string"));
		primitiveXsdTypes.put("char", new QName(Schema.SCHEMA_NS, "string"));
		primitiveXsdTypes.put("java.math.BigDecimal", new QName(Schema.SCHEMA_NS, "decimal"));
		primitiveXsdTypes.put("java.math.BigInteger", new QName(Schema.SCHEMA_NS, "int"));
		primitiveXsdTypes.put("java.net.URI", new QName(Schema.SCHEMA_NS, "anyURI"));
		primitiveXsdTypes.put("java.util.Calendar", new QName(Schema.SCHEMA_NS, "dateTime"));
		primitiveXsdTypes.put("java.util.Date", new QName(Schema.SCHEMA_NS, "dateTime"));
		primitiveXsdTypes.put("java.xml.QName", new QName(Schema.SCHEMA_NS, "QName"));
		primitiveXsdTypes.put("Object", new QName(Schema.SCHEMA_NS, "anyType"));
		primitiveXsdTypes.put("java.lang.Object", new QName(Schema.SCHEMA_NS, "anyType"));
	}
	
	@SuppressWarnings("unchecked")
	protected void initSchema(){
		//Check if namespace is present and grab its prefix
		int prefixNumber = 1;
		boolean namespaceAlreadyAdded = false;
		HashMap<String, String> namespaces = (HashMap<String, String>) wsdl.getNamespaceContext();
		for(String prefix : namespaces.keySet()){
			//Prevent prefix overlapping
			if(prefix.equals(schemaPrefix)){
				prefixNumber++;
				schemaPrefix = SCHEMA_BASE_PREFIX+prefixNumber;
			}
			//Get existent prefix if namespace is already added
			if(!enclosedSchema  && namespaces.get(prefix).equals(schema.getTargetNamespace())){
				schemaPrefix = prefix;
				namespaceAlreadyAdded = true;
				break;
			}
		}
				
		//Add namespace
		if(!namespaceAlreadyAdded)
			namespaces.put(schemaPrefix, schema.getTargetNamespace());
		//Add schema to wsdl
		if(enclosedSchema)
			wsdl.add(schema);
		schema.setAttributeFormDefault("qualified");
		schema.setElementFormDefault("qualified");
	}

	@Override
	public void addTypeService(IType type) {
		
		logger.debug("Adding type service '"+type.getElementName()+"'");
		
		String portTypeName = type.getElementName();
		
		PortType portType = null;
		if(wsdl.getPortType(portTypeName) != null){
			//Get port-type
			portType = wsdl.getPortType(portTypeName);
		}else{
			//Create new port-type
			portType = wsdl.newPortType(portTypeName);
		}
		
		try {
			for(IMethod method : type.getMethods()){
				//Ignore constructors and public methods
				if(!APIDesignAnalysisUtils.isServiceMethod(method)) continue;
				
				addPortTypeOperation(portType, method);
			}
		} catch (JavaModelException e) {
			e.printStackTrace();
		}

		//Add port type comments
		String typeComments = ASTUtils.getTypeComments(type);
		if(!typeComments.isEmpty()){
			Documentation portTypeDocumentation = new Documentation();
			portTypeDocumentation.setContent(escapeHtml(typeComments));
			portType.setDocumentation(portTypeDocumentation);
		}
	}
	
	protected void addPortTypeOperation(PortType portType, IMethod method){
		
		//Define operation name
		String operationName = method.getElementName();
		int i = 1;
		while(portType.getOperation(operationName) != null){
			i++;
			operationName = method.getElementName()+i;
		}
		
		logger.debug("Adding port type operation '"+operationName+"'");
		
		//Create operation object
		Operation operation = portType.newOperation(operationName);
		
		//Put method comments as operation comments
		String comments = ASTUtils.getMethodComments(method);
		if(!comments.isEmpty()){
			Documentation operationDocumentation = new Documentation();
			operationDocumentation.setContent(escapeHtml(comments));
			operation.setDocumentation(operationDocumentation);
		}
		
		//Add operation input element
		addOperationInput(operation, method);
		
		//Add operation output element
		addOperationOutput(operation, method);
		
		//Add fault elements
		addOperationFaults(operation, method);
	}
	
	protected void addOperationInput(Operation operation, IMethod method){
		
		String inputMessageName = operation.getName()+"Request";
		int i = 1;
		while(wsdl.getMessage(inputMessageName) != null){
			i++;
			inputMessageName = operation.getName()+"Request"+i;
		}
		
		logger.debug("Adding input message for operation '"+inputMessageName+"'");
		
		Message parametersMessage = operation.newInput("").newMessage(inputMessageName);
		
		Map<String, ITypeBinding> parametersTypes = new HashMap<String, ITypeBinding>();
		
		try{
			if(method.getParameters().length > 0){
				MethodDeclaration methodDeclaration = ASTUtils.parse(method);
				
				for(Object item : methodDeclaration.parameters())
					parametersTypes.put(((SingleVariableDeclaration) item).getName().toString(), ((SingleVariableDeclaration) item).getType().resolveBinding());
			}
		}catch(JavaModelException e){
			e.printStackTrace();
		}
		
		generateSchemaElement(inputMessageName, parametersTypes, true);
		parametersMessage.newPart("parameters", schemaPrefix+":"+inputMessageName); 
		
		//Add parameters documentation
		String comments = ASTUtils.getMethodParametersComments(method);
		if(!comments.isEmpty()){
			Documentation messageDocumentation = new Documentation();
			messageDocumentation.setContent(escapeHtml(comments));
			parametersMessage.setDocumentation(messageDocumentation);
		}
	}
	
	protected void addOperationOutput(Operation operation, IMethod method){
		
		String outputMessageName = operation.getName()+"Response";
		int i = 1;
		while(wsdl.getMessage(outputMessageName) != null){
			i++;
			outputMessageName = operation.getName()+"Response"+i;
		}
		
		ITypeBinding returnTypeBinding = ASTUtils.parse(method).getReturnType2().resolveBinding();
		
		if(returnTypeBinding.getName().equals("void")) return; //Don't add output message when operation return type is void
				
		logger.debug("Adding output message for operation '"+outputMessageName+"'");
		
		Message returnMessage = operation.newOutput("").newMessage(outputMessageName);
		
		Map<String, ITypeBinding> returnTypes = new HashMap<String, ITypeBinding>();
	
		returnTypes.put("return", returnTypeBinding);
		
		generateSchemaElement(outputMessageName, returnTypes, false);
		returnMessage.newPart("parameters", schemaPrefix+":"+outputMessageName); 

		//Add return documentation
		String comments = ASTUtils.getMethodReturnComments(method);
		if(!comments.isEmpty()){
			Documentation messageDocumentation = new Documentation();
			messageDocumentation.setContent(escapeHtml(comments));
			returnMessage.setDocumentation(messageDocumentation);
		}
	}
	
	protected void addOperationFaults(Operation operation, IMethod method){
		try {
			//No exceptions, prevent doing the AST parse
			if(method.getExceptionTypes().length == 0) 
				return;

			logger.debug("Adding fault messages for operation '"+method.getElementName()+"'");
			
			List<Fault> faults = new ArrayList<Fault>();
			IMethodBinding methodBinding = ASTUtils.parse(method).resolveBinding();
			
			ITypeBinding[] exceptionTypes = methodBinding.getExceptionTypes();
			
			if(exceptionTypes.length > 0){
				
				for(ITypeBinding exceptionType : exceptionTypes){
					
					Fault fault = new Fault();
					
					String faultMessageName = exceptionType.getName();
					
					Message faultMessage = wsdl.getMessage(faultMessageName); 
					if(faultMessage == null){
						faultMessage = wsdl.newMessage(faultMessageName);
						Map<String, ITypeBinding> faultTypes = new HashMap<String, ITypeBinding>();
						faultTypes.put("return", exceptionType);
						generateSchemaElement(exceptionType.getName()+"Fault", faultTypes, false);
						faultMessage.newPart("parameters", schemaPrefix+":"+exceptionType.getName()+"Fault"); 
					}
					
					fault.setDefinitions(wsdl);
					fault.setParent(operation);
					fault.setName(exceptionType.getName());
					fault.setMessage(faultMessage);
					faults.add(fault);
				}
				
			}

			if(faults.size() > 0){
				operation.setFaults(faults);
			}
			
		} catch (JavaModelException e) {
			//do nothing
		}
	}
	
	protected void generateSchemaElement(String name, Map<String, ITypeBinding> types, boolean isOperationParams){
		if(schema.getElement(name) != null) return; //Exit if element already exists
		Element schemaElement = schema.newElement(name);
		if(types.size() > 0){
			ComplexType complexType = schemaElement.newComplexType();
			Sequence sequence = complexType.newSequence();
			
			for(String partName : types.keySet()){
				Element e = sequence.newElement(partName, getComplexTypeQName(types.get(partName)));
				if(isOperationParams)
					e.setMinOccurs("0");
			}
		}else{
			//Empty message
			schemaElement.setType(getComplexTypeQName("void"));
		}
	}
	
	protected QName getComplexTypeQName(String qualifiedName){
		if(primitiveXsdTypes.get(qualifiedName) != null){
			return primitiveXsdTypes.get(qualifiedName);
		}else if(userDefinedTypes.get(qualifiedName) != null){
			return userDefinedTypes.get(qualifiedName);
		}else if(qualifiedName.equals("void")){
			//Create void type
			if(schema.getComplexType("void") == null){
				ComplexType voidType = schema.newComplexType("void");
				voidType.setName("void");
				voidType.newSequence();
			}
			QName qName = new QName(schema.getTargetNamespace(), "void");
			userDefinedTypes.put(qualifiedName, qName);
			return qName;
		}
		return null;
	}
	
	protected QName getComplexTypeQName(ITypeBinding typeBinding){
		String qualifiedName = typeBinding.getQualifiedName();
		
		QName qName = getComplexTypeQName(qualifiedName);
		if(qName != null)
			return qName;
		else{
			ComplexType complexType = generateComplexType(typeBinding);
			if(complexType != null){
				qName = new QName(schema.getTargetNamespace(), complexType.getName(), "tns");
				userDefinedTypes.put(qualifiedName, qName);
				return qName;
			}
		}
		return new QName(Schema.SCHEMA_NS, "anyType");
	}
	
	protected ComplexType generateComplexType(ITypeBinding typeBinding){
		
		if(typeBinding.isArray())
			//Java array type
			return generateArrayType(getComplexTypeQName(typeBinding.getElementType()));	
		else if(typeBinding.isParameterizedType()){
			
			ITypeBinding genericType = typeBinding.getErasure();
			
			if(javaArrayTypes.indexOf(genericType.getQualifiedName()) >= 0){
				ITypeBinding[] typeArguments = typeBinding.getTypeArguments();
				//Parametrized array type
				if(typeArguments.length > 0)
					return generateArrayType(getComplexTypeQName(typeArguments[0]));
				else
					return generateArrayType(new QName(Schema.SCHEMA_NS, "anyType"));

			}else if(javaMapTypes.indexOf(genericType.getQualifiedName()) >= 0){
				//Is parametrized map type
				ITypeBinding[] typeArguments = typeBinding.getTypeArguments();
				if(typeArguments.length > 1)
					return generateMapType(getComplexTypeQName(typeArguments[0]), getComplexTypeQName(typeArguments[1]));
			}
		}else{
			if(javaArrayTypes.indexOf(typeBinding.getQualifiedName()) >= 0)
				return generateArrayType(new QName(Schema.SCHEMA_NS, "anyType"));
			else if(javaMapTypes.indexOf(typeBinding.getQualifiedName()) >= 0)
				return generateMapType(new QName(Schema.SCHEMA_NS, "anyType"), new QName(Schema.SCHEMA_NS, "anyType"));
			else
				//Simple type
				return generateClassType(typeBinding);
		}
		return null;
	}
	
	protected ComplexType generateClassType(ITypeBinding typeBinding){
		
		//Check if it has setter/getter methods pair
		Map<String, ITypeBinding> publicAttributes = new HashMap<String, ITypeBinding>();
		Map<String, IMethodBinding> setterMethods = new HashMap<String, IMethodBinding>();
		for(IMethodBinding method : typeBinding.getDeclaredMethods())
			if(method.getName().startsWith("set") && method.getParameterTypes().length == 1)
				setterMethods.put(method.getName(), method);
		
		for(IMethodBinding method : typeBinding.getDeclaredMethods()){
			if(method.getName().startsWith("get") || method.getName().startsWith("is")){
				
				String setterName = method.getName().replaceAll("^(get|is)", "set");
				if(setterMethods.get(setterName) != null){
					
					IMethodBinding setterMethod = setterMethods.get(setterName);
					if(setterMethod.getParameterTypes()[0].getQualifiedName().equals(method.getReturnType().getQualifiedName())){
						String attributeName = method.getName().replaceAll("^(get|is)", "");
						publicAttributes.put(attributeName.substring(0, 1).toLowerCase()+attributeName.substring(1), method.getReturnType());
					}

				}
			}
		}
		
		//Get public fields
		for(IVariableBinding attribute : typeBinding.getDeclaredFields()){
			IField field = (IField) attribute.getJavaElement();
			if(ASTUtils.isPublic(field))
				publicAttributes.put(field.getElementName(), attribute.getType());
		}
		
		String classTypeName = typeBinding.getName();
		
		removeComplexType(classTypeName);
		ComplexType complexType = schema.newComplexType(classTypeName);
		complexType.setName(classTypeName);
		Sequence sequence = complexType.newSequence();
		//Add all atributes elements
		for(String attribute : publicAttributes.keySet()){
			ITypeBinding attributeType = publicAttributes.get(attribute);
			if(!attributeType.getQualifiedName().equals(typeBinding.getQualifiedName())){
				sequence.newElement(attribute, getComplexTypeQName(publicAttributes.get(attribute)));
			}
		}
		
		return complexType;
	}
	
	protected ComplexType generateArrayType(QName itemsType){
		String arrayTypeName = "ArrayOf"+StringUtils.capitalizeFirstLetter(itemsType.getLocalPart());
		
		removeComplexType(arrayTypeName);
		ComplexType arrayType = schema.newComplexType(arrayTypeName);
		arrayType.setName(arrayTypeName);
		Element itemElement = arrayType.newSequence().newElement("item", itemsType);
		itemElement.setMaxOccurs("unbounded");
		itemElement.setMinOccurs("0");
		return arrayType;
	}
	
	protected ComplexType generateMapType(QName keyType, QName valueType){
		
		String prefix = StringUtils.capitalizeFirstLetter(keyType.getLocalPart())+StringUtils.capitalizeFirstLetter(valueType.getLocalPart());
		String mapItemTypeName = prefix+"MapItem";
		String mapTypeName = prefix+"Map";
		
		removeComplexType(mapItemTypeName);
		ComplexType mapItemType = schema.newComplexType(mapItemTypeName);
		mapItemType.setName(mapItemTypeName);
		Sequence mapItemTypeSequence = mapItemType.newSequence();
		mapItemTypeSequence.newElement("key", keyType);
		mapItemTypeSequence.newElement("value", valueType);
		
		removeComplexType(mapTypeName);
		ComplexType mapType = schema.newComplexType(mapTypeName);
		mapType.setName(mapTypeName);
		Element itemElement = mapType.newSequence().newElement("item", mapItemType.getName());
		itemElement.setMaxOccurs("unbounded");
		itemElement.setMinOccurs("0");
		return mapType;
	}
	
	protected void removeComplexType(String name){
		if(schema.getComplexType(name) != null)
			schema.getComplexTypes().remove(schema.getComplexType(name));
	}

	protected String escapeHtml(String input){
		return input.replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("&", "&amp;");
	}
}
